/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.openide.awt; import java.awt.*; import java.awt.event.*; import java.beans.*; import javax.swing.*; import javax.swing.event.*; import org.openide.TopManager; import org.openide.util.actions.*; import org.openide.util.HelpCtx; import org.openide.util.WeakListener; /** Supporting class for manipulation with menu and toolbar presenters. * * @author Jaroslav Tulach */ public class Actions extends Object { /** Method that finds the keydescription assigned to this action. * @param action action to find key for * @return the text representing the key or null if there is no text assigned */ public static String findKey (SystemAction action) { TopManager t = TopManager.getDefault (); if (t == null) { return null; } KeyStroke[] arr = t.getGlobalKeymap ().getKeyStrokesForAction (action); if (arr.length == 0) { return null; } KeyStroke accelerator = arr[0]; int modifiers = accelerator.getModifiers(); String acceleratorText = ""; // NOI18N if (modifiers > 0) { acceleratorText = KeyEvent.getKeyModifiersText(modifiers); acceleratorText += "+"; // NOI18N } else if (accelerator.getKeyCode() == KeyEvent.VK_UNDEFINED) { return ""; // NOI18N } acceleratorText += KeyEvent.getKeyText(accelerator.getKeyCode()); return acceleratorText; } /** Attaches menu item to an action. * @param item menu item * @param action action * @param popup create popup or menu item */ public static void connect (JMenuItem item, SystemAction action, boolean popup) { Bridge b = new MenuBridge (item, action, popup); b.updateState (null); } /** Attaches checkbox menu item to boolean state action. * @param item menu item * @param action action * @param popup create popup or menu item */ public static void connect (JCheckBoxMenuItem item, BooleanStateAction action, boolean popup) { Bridge b = new CheckMenuBridge (item, action, popup); b.updateState (null); } /** Connects buttons to action. * @param button the button * @param action the action */ public static void connect (AbstractButton button, SystemAction action) { Bridge b = new ButtonBridge (button, action); b.updateState (null); } /** Connects buttons to action. * @param button the button * @param action the action */ public static void connect (AbstractButton button, BooleanStateAction action) { Bridge b = new BooleanButtonBridge (button, action); b.updateState (null); } /** Adds a change listener to TopManager's keymap. */ private static void addKeymapListener (PropertyChangeListener l) { TopManager t = TopManager.getDefault (); if (t != null) { t.addPropertyChangeListener (l); } } /** Find key strokes for action. * @param action * @return array of keystrokes that invoke that action */ private static KeyStroke[] getKeyStrokesForAction (SystemAction action) { TopManager t = TopManager.getDefault (); if (t != null) { return t.getGlobalKeymap ().getKeyStrokesForAction (action); } else { return new KeyStroke[0]; } } /** Sets the text for the menu item. Cut from the name '&' char. * @param item MenuItem * @param text new label * @param useMnemonic if true and '&' char found in new text, next char is used * as Mnemonic. */ static void setMenuText(JMenuItem item, String text, boolean useMnemonic) { int i = text.indexOf('&'); String newText = text; if (i < 0) { item.setText(text); } else { item.setText(text.substring(0, i) + text.substring(i + 1)); if (useMnemonic) { item.setMnemonic(text.charAt(i + 1)); } } } /** Cuts first occurence of '&' and * @return string without first '&' if there was any. */ public static String cutAmpersand(String text) { int i = text.indexOf('&'); return (i < 0) ? text : (text.substring(0, i) + text.substring(i + 1)); } /** Listener on showing/hiding state of the component. * Is attached to menu or toolbar item in prepareXXX methods and * method addNotify is called when the item is showing and * the method removeNotify is called when the item is hidding. * <P> * There is a special support listening on changes in the action and * if such change occures, updateState method is called to * reflect it. */ private static abstract class Bridge extends Object implements PropertyChangeListener { /** component to work with */ protected JComponent comp; /** action to associate */ protected SystemAction action; /** property change listener. */ private PropertyChangeListener propL; /** @param comp component * @param action the action */ public Bridge (JComponent comp, SystemAction action) { this.comp = comp; this.action = action; // attaches visibility listener safely in event thread Bridge.this.comp.addPropertyChangeListener (Bridge.this); if (Bridge.this.comp.isShowing ()) { addNotify (); } // listener on keys used for this object addKeymapListener (WeakListener.propertyChange (this, TopManager.getDefault ())); // associate context help, if applicable HelpCtx help = action.getHelpCtx (); if (help != null && ! help.equals (HelpCtx.DEFAULT_HELP) && help.getHelpID () != null) HelpCtx.setHelpIDString (comp, help.getHelpID ()); } /** Attaches listener to given action */ public void addNotify () { propL = WeakListener.propertyChange (this, action); action.addPropertyChangeListener (propL); updateState (null); } /** Remove the listener */ public void removeNotify () { action.removePropertyChangeListener (propL); } /** @param changedProperty the name of property that has changed * or null if it is not known */ public abstract void updateState (String changedProperty); /** Listener to changes of some properties. * Multicast - reacts to keymap changes and ancestor changes * together. */ public void propertyChange (final PropertyChangeEvent ev) { // fire later javax.swing.SwingUtilities.invokeLater (new Runnable () { public void run () { if ("ancestor".equals(ev.getPropertyName())) { // ancestor change - decide if parent is null or not if (ev.getNewValue() != null) addNotify(); else removeNotify(); return; } updateState (ev.getPropertyName ()); } }); } } /** Bridge between an action and button. */ private static class ButtonBridge extends Bridge { /** the button */ protected AbstractButton button; public ButtonBridge (AbstractButton button, SystemAction action) { super (button, action); button.addActionListener (action); this.button = button; } /** @param changedProperty the name of property that has changed * or null if it is not known */ public void updateState (String changedProperty) { if (changedProperty == null || changedProperty.equals ("enabled")) { // NOI18N button.setEnabled (action.isEnabled ()); } if (changedProperty == null || changedProperty.equals ("icon")) { // NOI18N button.setIcon (action.getIcon ()); } if (changedProperty == null || changedProperty.equals (TopManager.PROP_GLOBAL_KEYMAP)) { String tip = findKey (action); if (tip == null || tip.equals("")) { // NOI18N button.setToolTipText(cutAmpersand(action.getName())); } else { button.setToolTipText(java.text.MessageFormat.format ( org.openide.util.NbBundle.getBundle(Actions.class).getString("FMT_ButtonHint"), new Object[] { cutAmpersand(action.getName()), tip } ) ); } } } } /** Bridge for button and boolean action. */ private static class BooleanButtonBridge extends ButtonBridge { public BooleanButtonBridge (AbstractButton button, BooleanStateAction action) { super (button, action); } /** @param changedProperty the name of property that has changed * or null if it is not known */ public void updateState (String changedProperty) { super.updateState (changedProperty); if (changedProperty == null || changedProperty.equals (BooleanStateAction.PROP_BOOLEAN_STATE)) { button.setSelected (((BooleanStateAction)action).getBooleanState ()); } } } /** Menu item bridge. */ private static class MenuBridge extends ButtonBridge { /** behave like menu or popup */ private boolean popup; /** Constructor. * @param popup pop-up menu */ public MenuBridge (JMenuItem item, SystemAction action, boolean popup) { super (item, action); this.popup = popup; if (popup) { prepareMargins (item, action); } } /** @param changedProperty the name of property that has changed * or null if it is not known */ public void updateState (String changedProperty) { if (changedProperty == null || changedProperty.equals ("enabled")) { // NOI18N button.setEnabled (action.isEnabled ()); } if (changedProperty == null || !changedProperty.equals ("accelerator")) { // NOI18N updateKey ((JMenuItem)comp, action); } if (!popup) { if (changedProperty == null || changedProperty.equals ("icon")) { // NOI18N button.setIcon (action.getIcon ()); } } if (changedProperty == null || changedProperty.equals ("name")) { // NOI18N setMenuText (((JMenuItem)comp), action.getName (), !popup); } } } /** Check menu item bridge. */ private static final class CheckMenuBridge extends BooleanButtonBridge { /** is popup or menu */ private boolean popup; /** Popup menu */ public CheckMenuBridge (JCheckBoxMenuItem item, BooleanStateAction action, boolean popup) { super (item, action); this.popup = popup; if (popup) { prepareMargins (item, action); } } /** @param changedProperty the name of property that has changed * or null if it is not known */ public void updateState (String changedProperty) { super.updateState (changedProperty); updateKey ((JMenuItem)comp, action); if (changedProperty == null || changedProperty.equals ("name")) { // NOI18N setMenuText (((JMenuItem)comp), action.getName (), !popup); } } } /** Sub menu bridge. */ private static final class SubMenuBridge extends MenuBridge implements ChangeListener { /** model to obtain subitems from */ private SubMenuModel model; /** submenu */ private SubMenu menu; /** Constructor. */ public SubMenuBridge (SubMenu item, SystemAction action, SubMenuModel model, boolean popup) { super (item, action, popup); prepareMargins (item, action); menu = item; this.model = model; model.addChangeListener (WeakListener.change (this, model)); } public void addNotify () { super.addNotify (); generateSubMenu (); } /** Called when model changes. Regenerates the model. */ public void stateChanged (ChangeEvent ev) { // change in keys or in submenu model generateSubMenu (); } /** Regenerates the menu */ private void generateSubMenu() { boolean shouldUpdate = false; try { menu.removeAll (); int cnt = model.getCount (); if (cnt != menu.previousCount) { // update UI shouldUpdate = true; } // in all cases remeber the previous menu.previousCount = cnt; // remove if there is an previous listener if (menu.oneItemListener != null) { menu.removeActionListener(menu.oneItemListener); } if (cnt == 0) { // menu disabled menu.setEnabled (false); return; } else { menu.setEnabled (true); // go on } if (cnt == 1) { // generate without submenu menu.addActionListener(menu.oneItemListener = new ISubActionListener(0, model)); HelpCtx help = model.getHelpCtx (0); associateHelp (menu, help == null ? action.getHelpCtx () : help); } else { for (int i = 0; i < model.getCount(); i++) { String label = model.getLabel(i); // MenuShortcut shortcut = support.getMenuShortcut(i); if (label == null) menu.addSeparator(); else { // if (shortcut == null) JMenuItem item = new JMenuItem(label); // else // item = new JMenuItem(label, shortcut); item.addActionListener(new ISubActionListener(i, model)); HelpCtx help = model.getHelpCtx (i); associateHelp (item, help == null ? action.getHelpCtx () : help); menu.add(item); } } associateHelp (menu, action.getHelpCtx ()); } } finally { if (shouldUpdate) { menu.updateUI (); } } } private void associateHelp (JComponent comp, HelpCtx help) { if (help != null && ! help.equals (HelpCtx.DEFAULT_HELP) && help.getHelpID () != null) HelpCtx.setHelpIDString (comp, help.getHelpID ()); else HelpCtx.setHelpIDString (comp, null); } /** The class that listens to the menu item selections and forwards it to the * action class via the performAction() method. */ private static class ISubActionListener implements java.awt.event.ActionListener { int index; SubMenuModel support; public ISubActionListener(int index, SubMenuModel support) { this.index = index; this.support = support; } /** called when a user clicks on this menu item */ public void actionPerformed(ActionEvent e) { support.performActionAt(index); } } } // // Methods for configuration of MenuItems // /** Method to prepare the margins and text positions. */ static void prepareMargins (JMenuItem item, SystemAction action) { Insets margin = item.getMargin (); margin.left = 0; item.setMargin(margin); item.setHorizontalTextPosition(JMenuItem.RIGHT); item.setHorizontalAlignment(JMenuItem.LEFT); } /** Updates value of the key * @param item item to update * @param action the action to update */ static void updateKey (JMenuItem item, SystemAction action) { if (!(item instanceof JMenu)) { // menu does not have accelerators // key KeyStroke[] arr = getKeyStrokesForAction (action); if (arr.length != 0) { // assign the key item.setAccelerator (arr[0]); } else { item.setAccelerator (null); } } } // // // The presenter classes // // /** Actions.MenuItem extends the java.awt.MenuItem and adds a connection to Corona * system actions. The ActMenuItem processes the MenuEvents itself and * calls the action.performAction() method. * It also tracks the enabled state of the action and reflects it as its * visual enabled state. * */ public static class MenuItem extends javax.swing.JMenuItem { static final long serialVersionUID =-21757335363267194L; /** Constructs a new ActMenuItem with the specified label * and no keyboard shortcut and connects it to the given SystemAction. * @param action the action to which this menu item should be connected * @param label a string label for the check box menu item, * or null for an unlabeled menu item. * @param showIcon if true, the menu item has an icon of the action * @param useMnemonic if true, the menu try to find mnemonic in action label */ public MenuItem (SystemAction aAction, boolean useMnemonic) { Actions.connect (this, aAction, !useMnemonic); } } /** CheckboxMenuItem extends the java.awt.CheckboxMenuItem and adds * a connection to Corona boolean state actions. The ActCheckboxMenuItem * processes the ItemEvents itself and calls the action.seBooleanState() method. * It also tracks the enabled and boolean state of the action and reflects it * as its visual enabled/check state. * * @author Ian Formanek, Jan Jancura */ public static class CheckboxMenuItem extends javax.swing.JCheckBoxMenuItem { static final long serialVersionUID =6190621106981774043L; /** Constructs a new ActCheckboxMenuItem with the specified label * and connects it to the given BooleanStateAction. * @param action the action to which this menu item should be connected * @param label a string label for the check box menu item, * or null for an unlabeled menu item. * @param useMnemonic if true, the menu try to find mnemonic in action label */ public CheckboxMenuItem (BooleanStateAction aAction, boolean useMnemonic) { Actions.connect (this, aAction, !useMnemonic); } } /** Component shown in toolbar, representing an action. * */ public static class ToolbarButton extends org.openide.awt.ToolbarButton { static final long serialVersionUID =6564434578524381134L; public ToolbarButton (SystemAction aAction) { super (null); Actions.connect (this, aAction); } /** * Gets the maximum size of this component. * @return A dimension object indicating this component's maximum size. * @see #getMinimumSize * @see #getPreferredSize * @see LayoutManager */ public Dimension getMaximumSize() { return this.getPreferredSize (); } public Dimension getMinimumSize() { return this.getPreferredSize (); } } /** The Component for BooleeanState action that is to be shown * in a toolbar. * */ public static class ToolbarToggleButton extends org.openide.awt.ToolbarToggleButton { static final long serialVersionUID =-4783163952526348942L; /** Constructs a new ActToolbarToggleButton for specified action */ public ToolbarToggleButton (BooleanStateAction aAction) { super(null, false); Actions.connect (this, aAction); } /** * Gets the maximum size of this component. * @return A dimension object indicating this component's maximum size. * @see #getMinimumSize * @see #getPreferredSize * @see LayoutManager */ public Dimension getMaximumSize() { return this.getPreferredSize (); } public Dimension getMinimumSize() { return this.getPreferredSize (); } } /** Interface for the creating Actions.SubMenu. It provides the methods for * all items in submenu: name shortcut and perform method. Also has methods * for notification of changes of the model. */ public static interface SubMenuModel { /** @return count of the submenu items. */ public int getCount(); /** Gets label for specific index * @index of the submenu item * @return label for this menu item */ public String getLabel(int index); /** Gets shortcut for specific index * @index of the submenu item * @return menushortcut for this menu item */ // public MenuShortcut getMenuShortcut(int index); /** Get context help for the specified item. * This can be used to associate help with individual items. * You may return <code>null</code> to just use the context help for * the associated system action (if any). * Note that only help IDs will work, not URLs. * @return the context help, or <code>null</code> */ public HelpCtx getHelpCtx (int index); /** Perform the action on the specific index * @index of the submenu item which should be performed */ public void performActionAt(int index); /** Adds change listener for changes of the model. */ public void addChangeListener (ChangeListener l); /** Removes change listener for changes of the model. */ public void removeChangeListener (ChangeListener l); } /** SubMenu provides easy way of displaying submenu items based on * SubMenuModel. */ public static class SubMenu extends org.openide.awt.JMenuPlus { /** number of previous sub items */ int previousCount = -1; /** listener to remove from this menu or <CODE>null</CODE> */ ActionListener oneItemListener; /** Constructs a new ActMenuItem with the specified label * and no keyboard shortcut and connects it to the given SystemAction. * No icon is used by default. * @param action the action to which this menu item should be connected * @param label a string label for the check box menu item, * or null for an unlabeled menu item. * @param support the support for the menu items */ public SubMenu(SystemAction aAction, SubMenuModel model) { this (aAction, model, true); } static final long serialVersionUID =-4446966671302959091L; /** Constructs a new ActMenuItem with the specified label * and no keyboard shortcut and connects it to the given SystemAction. * No icon is used by default. * @param action the action to which this menu item should be connected * @param label a string label for the check box menu item, * or null for an unlabeled menu item. * @param support the support for the menu items */ public SubMenu(SystemAction aAction, SubMenuModel model, boolean popup) { new SubMenuBridge (this, aAction, model, popup).updateState (null); } /** Request for either MenuUI or MenuItemUI if the only one subitem should not * use submenu. */ public String getUIClassID () { if (previousCount == 0) { return "MenuItemUI"; // NOI18N } return previousCount == 1 ? "MenuItemUI" : "MenuUI"; // NOI18N } public void menuSelectionChanged(boolean isIncluded) { if (previousCount == 1) setArmed(isIncluded); // JMenuItem behaviour else super.menuSelectionChanged(isIncluded); } /** Menu cannot be selected when it represents MenuItem. */ public void setSelected (boolean s) { // disabled menu cannot be selected if (isEnabled () || !s) { super.setSelected (s); } } /** Seting menu to disabled also sets the item as not selected */ public void setEnabled (boolean e) { super.setEnabled (e); if (!e) { super.setSelected (false); } } public void doClick(int pressTime) { if (!isEnabled ()) { // do nothing if not enabled return; } if (oneItemListener != null) { oneItemListener.actionPerformed (null); } else { super.doClick (pressTime); } } } } /* * Log * 33 src-jtulach1.32 1/20/00 Libor Kramolis * 32 src-jtulach1.31 1/18/00 Libor Kramolis * 31 src-jtulach1.30 1/13/00 Ian Formanek NOI18N * 30 src-jtulach1.29 1/13/00 Ian Formanek I18N * 29 src-jtulach1.28 1/12/00 Ian Formanek NOI18N * 28 src-jtulach1.27 1/7/00 Ian Formanek Accelerator key displayed * on popup menus as well * 27 src-jtulach1.26 1/6/00 Jesse Glick #2279 -- context help for * submenu presenters with only one item in the model. * 26 src-jtulach1.25 11/5/99 Jaroslav Tulach WeakListener has now * registration methods. * 25 src-jtulach1.24 10/22/99 Ian Formanek NO SEMANTIC CHANGE - Sun * Microsystems Copyright in File Comment * 24 src-jtulach1.23 8/17/99 Ian Formanek Generated serial version * UID * 23 src-jtulach1.22 8/13/99 Jaroslav Tulach New Main Explorer * 22 src-jtulach1.21 8/9/99 Ian Formanek Generated Serial Version * UID * 21 src-jtulach1.20 8/5/99 Jaroslav Tulach Tools & New action in * editor. * 20 src-jtulach1.19 7/16/99 Jesse Glick Actions.SubMenuModel now * has context help. * 19 src-jtulach1.18 7/12/99 Jesse Glick Context help. * 18 src-jtulach1.17 6/28/99 Ian Formanek NbJMenu renamed to * JMenuPlus * 17 src-jtulach1.16 6/28/99 Ian Formanek Fixed bug 2043 - It is * virtually impossible to choose lower items of New From Template from * popup menu on 1024x768 * 16 src-jtulach1.15 6/8/99 Ian Formanek ---- Package Change To * org.openide ---- * 15 src-jtulach1.14 5/18/99 Ian Formanek Undone last change as it * caused popup menus to be weird... * 14 src-jtulach1.13 5/17/99 Miloslav Metelka invokeLater() in all * updateStatus() * 13 src-jtulach1.12 5/5/99 Jaroslav Tulach Works with 122 * 12 src-jtulach1.11 4/1/99 David Simonek separators added to * docking action * 11 src-jtulach1.10 3/26/99 Jesse Glick BooleanStateAction.PROP_BOOLEAN_STATE * is now public. * 10 src-jtulach1.9 3/21/99 Jaroslav Tulach Keys. * 9 src-jtulach1.8 3/17/99 Ian Formanek Fixed BooleanStateAction * behavior * 8 src-jtulach1.7 3/11/99 Jaroslav Tulach SubMenu regenerates menu * on addNotify. * 7 src-jtulach1.6 3/9/99 Jaroslav Tulach Node actions releases * sometimes its listeners. * 6 src-jtulach1.5 3/4/99 David Simonek * 5 src-jtulach1.4 3/4/99 Jaroslav Tulach Keymap change is fired by * TopManager * 4 src-jtulach1.3 3/2/99 Jaroslav Tulach Icon changes * 3 src-jtulach1.2 3/2/99 Jaroslav Tulach * 2 src-jtulach1.1 3/2/99 Jaroslav Tulach * 1 src-jtulach1.0 3/1/99 Jaroslav Tulach * $ */